//Copyright 2003-2005 Arthur van Hoff, Rick Blair
//Licensed under Apache License version 2.0
//Original license LGPL
package javax.jmdns.impl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.TimerTask;
import java.util.Vector;

import javax.jmdns.ServiceInfo;
import javax.jmdns.impl.DNSRecord.Pointer;
import javax.jmdns.impl.DNSRecord.Service;
import javax.jmdns.impl.DNSRecord.Text;

/** JmDNS service information.
 * @version %I%, %G%
 * @author Arthur van Hoff, Jeff Sonstein, Werner Randelshofer */
public class ServiceInfoImpl extends ServiceInfo implements DNSListener {
	private JmDNSImpl dns;
	// State machine
	/** The state of this service info. This is used only for services announced
	 * by JmDNS.
	 * <p/>
	 * For proper handling of concurrency, this variable must be changed only
	 * using methods advanceState(), revertState() and cancel(). */
	private DNSState state = DNSState.PROBING_1;
	/** Task associated to this service info. Possible tasks are JmDNS.Prober,
	 * JmDNS.Announcer, JmDNS.Responder, JmDNS.Canceler. */
	private TimerTask task;
	String type;
	private String name;
	String server;
	int port;
	int weight;
	int priority;
	private byte text[];
	@SuppressWarnings("rawtypes")
	Hashtable props;
	InetAddress addr;

	/** @see javax.jmdns.ServiceInfo#create(String, String, int, String) */
	public ServiceInfoImpl(String type, String name, int port, String text) {
		this(type, name, port, 0, 0, text);
	}

	/** @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, String) */
	public ServiceInfoImpl(String type, String name, int port, int weight,
			int priority, String text) {
		this(type, name, port, weight, priority, (byte[]) null);
		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream(text.length());
			writeUTF(out, text);
			byte[] data = out.toByteArray();
			this.setText(new byte[data.length + 1]);
			this.getText()[0] = (byte) data.length;
			System.arraycopy(data, 0, this.getText(), 1, data.length);
		} catch (IOException e) {
			throw new RuntimeException("unexpected exception: " + e);
		}
	}

	/** @see javax.jmdns.ServiceInfo#create(String, String, int, int, int,
	 * Hashtable) */
	@SuppressWarnings("rawtypes")
	public ServiceInfoImpl(String type, String name, int port, int weight,
			int priority, Hashtable props) {
		this(type, name, port, weight, priority, new byte[0]);
		if (props != null) {
			try {
				ByteArrayOutputStream out = new ByteArrayOutputStream(256);
				for (Enumeration e = props.keys(); e.hasMoreElements();) {
					String key = (String) e.nextElement();
					Object val = props.get(key);
					ByteArrayOutputStream out2 = new ByteArrayOutputStream(100);
					writeUTF(out2, key);
					if (val instanceof String) {
						out2.write('=');
						writeUTF(out2, (String) val);
					} else {
						if (val instanceof byte[]) {
							out2.write('=');
							byte[] bval = (byte[]) val;
							out2.write(bval, 0, bval.length);
						} else {
							if (val != NO_VALUE) {
								throw new IllegalArgumentException(
										"invalid property value: " + val);
							}
						}
					}
					byte data[] = out2.toByteArray();
					out.write(data.length);
					out.write(data, 0, data.length);
				}
				this.setText(out.toByteArray());
			} catch (IOException e) {
				throw new RuntimeException("unexpected exception: " + e);
			}
		}
	}

	/** @see javax.jmdns.ServiceInfo#create(String, String, int, int, int, byte[]) */
	public ServiceInfoImpl(String type, String name, int port, int weight,
			int priority, byte text[]) {
		this.type = type;
		this.name = name;
		this.port = port;
		this.weight = weight;
		this.priority = priority;
		this.setText(text);
	}

	/** Construct a service record during service discovery. */
	ServiceInfoImpl(String type, String name) {
		if (!type.endsWith(".")) {
			throw new IllegalArgumentException(
					"type must be fully qualified DNS name ending in '.': "
							+ type);
		}
		this.type = type;
		this.name = name;
	}

	/** During recovery we need to duplicate service info to reregister them */
	ServiceInfoImpl(ServiceInfoImpl info) {
		if (info != null) {
			this.type = info.type;
			this.name = info.name;
			this.port = info.port;
			this.weight = info.weight;
			this.priority = info.priority;
			this.setText(info.getText());
		}
	}

	/** @see javax.jmdns.ServiceInfo#getType() */
	public String getType() {
		return type;
	}

	/** @see javax.jmdns.ServiceInfo#getName() */
	public String getName() {
		return name;
	}

	/** Sets the service instance name.
	 * @param name
	 * unqualified service instance name, such as <code>foobar</code> */
	void setName(String name) {
		this.name = name;
	}

	/** @see javax.jmdns.ServiceInfo#getQualifiedName() */
	public String getQualifiedName() {
		return name + "." + type;
	}

	/** @see javax.jmdns.ServiceInfo#getServer() */
	public String getServer() {
		return server;
	}

	/** @see javax.jmdns.ServiceInfo#getHostAddress() */
	public String getHostAddress() {
		return (addr != null ? addr.getHostAddress() : "");
	}

	public InetAddress getAddress() {
		return addr;
	}

	/** @see javax.jmdns.ServiceInfo#getInetAddress() */
	public InetAddress getInetAddress() {
		return addr;
	}

	/** @see javax.jmdns.ServiceInfo#getPort() */
	public int getPort() {
		return port;
	}

	/** @see javax.jmdns.ServiceInfo#getPriority() */
	public int getPriority() {
		return priority;
	}

	/** @see javax.jmdns.ServiceInfo#getWeight() */
	public int getWeight() {
		return weight;
	}

	/** @see javax.jmdns.ServiceInfo#getTextBytes() */
	public byte[] getTextBytes() {
		return getText();
	}

	/** @see javax.jmdns.ServiceInfo#getTextString() */
	public String getTextString() {
		if ((getText() == null) || (getText().length == 0)
				|| ((getText().length == 1) && (getText()[0] == 0))) {
			return null;
		}
		return readUTF(getText(), 0, getText().length);
	}

	/** @see javax.jmdns.ServiceInfo#getURL() */
	public String getURL() {
		return getURL("http");
	}

	/** @see javax.jmdns.ServiceInfo#getURL(java.lang.String) */
	public String getURL(String protocol) {
		String url = protocol + "://" + getHostAddress() + ":" + getPort();
		String path = getPropertyString("path");
		if (path != null) {
			if (path.indexOf("://") >= 0) {
				url = path;
			} else {
				url += path.startsWith("/") ? path : "/" + path;
			}
		}
		return url;
	}

	/** @see javax.jmdns.ServiceInfo#getPropertyBytes(java.lang.String) */
	public synchronized byte[] getPropertyBytes(String name) {
		return (byte[]) getProperties().get(name);
	}

	/** @see javax.jmdns.ServiceInfo#getPropertyString(java.lang.String) */
	public synchronized String getPropertyString(String name) {
		byte data[] = (byte[]) getProperties().get(name);
		if (data == null) {
			return null;
		}
		if (data == NO_VALUE) {
			return "true";
		}
		return readUTF(data, 0, data.length);
	}

	/** @see javax.jmdns.ServiceInfo#getPropertyNames() */
	@SuppressWarnings("rawtypes")
	public Enumeration getPropertyNames() {
		Hashtable props = getProperties();
		return (props != null) ? props.keys() : new Vector().elements();
	}

	/** Write a UTF string with a length to a stream. */
	void writeUTF(OutputStream out, String str) throws IOException {
		for (int i = 0, len = str.length(); i < len; i++) {
			int c = str.charAt(i);
			if ((c >= 0x0001) && (c <= 0x007F)) {
				out.write(c);
			} else {
				if (c > 0x07FF) {
					out.write(0xE0 | ((c >> 12) & 0x0F));
					out.write(0x80 | ((c >> 6) & 0x3F));
					out.write(0x80 | ((c >> 0) & 0x3F));
				} else {
					out.write(0xC0 | ((c >> 6) & 0x1F));
					out.write(0x80 | ((c >> 0) & 0x3F));
				}
			}
		}
	}

	/** Read data bytes as a UTF stream. */
	String readUTF(byte data[], int off, int len) {
		StringBuffer buf = new StringBuffer();
		for (int end = off + len; off < end;) {
			int ch = data[off++] & 0xFF;
			switch (ch >> 4) {
			case 0:
			case 1:
			case 2:
			case 3:
			case 4:
			case 5:
			case 6:
			case 7:
				// 0xxxxxxx
				break;
			case 12:
			case 13:
				if (off >= len) {
					return null;
				}
				// 110x xxxx 10xx xxxx
				ch = ((ch & 0x1F) << 6) | (data[off++] & 0x3F);
				break;
			case 14:
				if (off + 2 >= len) {
					return null;
				}
				// 1110 xxxx 10xx xxxx 10xx xxxx
				ch = ((ch & 0x0f) << 12) | ((data[off++] & 0x3F) << 6)
						| (data[off++] & 0x3F);
				break;
			default:
				if (off + 1 >= len) {
					return null;
				}
				// 10xx xxxx, 1111 xxxx
				ch = ((ch & 0x3F) << 4) | (data[off++] & 0x0f);
				break;
			}
			buf.append((char) ch);
		}
		return buf.toString();
	}

	@SuppressWarnings({ "rawtypes", "unchecked" })
	synchronized Hashtable getProperties() {
		if ((props == null) && (getText() != null)) {
			Hashtable props = new Hashtable();
			int off = 0;
			while (off < getText().length) {
				// length of the next key value pair
				int len = getText()[off++] & 0xFF;
				if ((len == 0) || (off + len > getText().length)) {
					props.clear();
					break;
				}
				// look for the '='
				int i = 0;
				for (; (i < len) && (getText()[off + i] != '='); i++) {
					;
				}
				// get the property name
				String name = readUTF(getText(), off, i);
				if (name == null) {
					props.clear();
					break;
				}
				if (i == len) {
					props.put(name, NO_VALUE);
				} else {
					byte value[] = new byte[len - ++i];
					System.arraycopy(getText(), off + i, value, 0, len - i);
					props.put(name, value);
					off += len;
				}
			}
			this.props = props;
		}
		return props;
	}

	/** JmDNS callback to update a DNS record. */
	public void updateRecord(JmDNSImpl jmdns, long now, DNSRecord rec) {
		if ((rec != null) && !rec.isExpired(now)) {
			switch (rec.type) {
			case DNSConstants.TYPE_A: // IPv4
			case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This
											// has not been tested
				if (rec.name.equals(server)) {
					addr = ((DNSRecord.Address) rec).getAddress();
				}
				break;
			case DNSConstants.TYPE_SRV:
				if (rec.name.equals(getQualifiedName())) {
					DNSRecord.Service srv = (DNSRecord.Service) rec;
					server = srv.server;
					port = srv.port;
					weight = srv.weight;
					priority = srv.priority;
					addr = null;
					// changed to use getCache() instead - jeffs
					// updateRecord(jmdns, now,
					// (DNSRecord)jmdns.cache.get(server, TYPE_A, CLASS_IN));
					updateRecord(
							jmdns,
							now,
							(DNSRecord) jmdns.getCache().get(server,
									DNSConstants.TYPE_A, DNSConstants.CLASS_IN));
				}
				break;
			case DNSConstants.TYPE_TXT:
				if (rec.name.equals(getQualifiedName())) {
					DNSRecord.Text txt = (DNSRecord.Text) rec;
					setText(txt.text);
				}
				break;
			}
			// Future Design Pattern
			// This is done, to notify the wait loop in method
			// JmDNS.getServiceInfo(type, name, timeout);
			if (hasData() && getDns() != null) {
				getDns().handleServiceResolved(this);
				setDns(null);
			}
			synchronized (this) {
				notifyAll();
			}
		}
	}

	/** Returns true if the service info is filled with data. */
	public boolean hasData() {
		return server != null && addr != null && getText() != null;
	}

	// State machine
	/** Sets the state and notifies all objects that wait on the ServiceInfo. */
	public synchronized void advanceState() {
		state = state.advance();
		notifyAll();
	}

	/** Sets the state and notifies all objects that wait on the ServiceInfo. */
	synchronized void revertState() {
		state = state.revert();
		notifyAll();
	}

	/** Sets the state and notifies all objects that wait on the ServiceInfo. */
	synchronized void cancel() {
		state = DNSState.CANCELED;
		notifyAll();
	}

	/** Returns the current state of this info. */
	public DNSState getState() {
		return state;
	}

	public int hashCode() {
		return getQualifiedName().hashCode();
	}

	public boolean equals(Object obj) {
		return (obj instanceof ServiceInfoImpl)
				&& getQualifiedName().equals(
						((ServiceInfoImpl) obj).getQualifiedName());
	}

	public String getNiceTextString() {
		StringBuffer buf = new StringBuffer();
		for (int i = 0, len = getText().length; i < len; i++) {
			if (i >= 20) {
				buf.append("...");
				break;
			}
			int ch = getText()[i] & 0xFF;
			if ((ch < ' ') || (ch > 127)) {
				buf.append("\\0");
				buf.append(Integer.toString(ch, 8));
			} else {
				buf.append((char) ch);
			}
		}
		return buf.toString();
	}

	public String toString() {
		StringBuffer buf = new StringBuffer();
		buf.append("service[");
		buf.append(getQualifiedName());
		buf.append(',');
		buf.append(getAddress());
		buf.append(':');
		buf.append(port);
		buf.append(',');
		buf.append(getNiceTextString());
		buf.append(']');
		return buf.toString();
	}

	public void addAnswers(DNSOutgoing out, int ttl, HostInfo localHost)
			throws IOException {
		out.addAnswer(new Pointer(type, DNSConstants.TYPE_PTR,
				DNSConstants.CLASS_IN, ttl, getQualifiedName()), 0);
		out.addAnswer(new Service(getQualifiedName(), DNSConstants.TYPE_SRV,
				DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, ttl,
				priority, weight, port, localHost.getName()), 0);
		out.addAnswer(new Text(getQualifiedName(), DNSConstants.TYPE_TXT,
				DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE, ttl,
				getText()), 0);
		// out.addAnswer(rec, 0);
		// if(localHost.getAddress() != null) {
		// out.addAuthorativeAnswer(new DNSRecord.Address(getQualifiedName(),
		// DNSConstants.TYPE_A, DNSConstants.CLASS_IN, ttl,
		// localHost.getAddress()));
		// }
	}

	public void setTask(TimerTask task) {
		this.task = task;
	}

	public TimerTask getTask() {
		return task;
	}

	public void setText(byte[] text) {
		this.text = text;
	}

	public byte[] getText() {
		return text;
	}

	public void setDns(JmDNSImpl dns) {
		this.dns = dns;
	}

	public JmDNSImpl getDns() {
		return dns;
	}
}